1.let
基本用法
1
2
3
4
5
6
7for(let i =0;i < 3;i++){
let i = 'abc';
console.log(i);
}
//abc
//abc
//abc for循环有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。上述代码表明函数内部的变量 i 与循环变量 i 不在同一个作用域,有各自单独的作用域。
不存在变量提升
1
2
3
4
5
6
7//var 的情况
console.log(foo);//undefined
var foo=2;
//let 的情况
console.log(bar);//ReferenceError: foo is not defined
let bar=2; 上述代码表明,使用 var 声明变量,会发生变量提升,即在脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出 undefined。使用 let 声明变量,不会发生变量提升。这表示在声明它之前,变量 bar 是不存在的,这时如果用到它,就会抛出一个错误。
暂时性死区
只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
1
2
3
4
5
6var tmp=123;
if(true){
tmp='abc';//ReferenceError
let tmp;
} 上述代码中,存在全局变量tmp,但是在块级作用域内 let 又声明了一个局部变量 tmp,导致后者绑定这个块级作用域,不受外部影响,所以在 let 声明变量前,对tmp 赋值会报错。
再举一个更详细的例子,如下:
1
2
3
4
5
6
7
8
9
10
11if (true) {
// Temporal dead zone (TDZ) 开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
} 上述代码中,在 let 命令声明变量 tmp 前,都属于变量 tmp 的死区。
“暂时性死区”也意味着 typeof 不再是一个百分之百安全的操作。
1 | typeof x; // ReferenceError |
上述代码通过比较,表明在没有 let 之前,typeof 运算符是百分之百安全的,永远不会报错。使用 let 声明变量,在变量没有声明之前就使用,会抛出一个ReferenceError的错误。
1 | function bar(x = y, y = 2) { |
上述代码报错,也是因为死区,不过这个死区比较隐蔽。参数 x 默认值等于另一个参数 y ,而此时 y 还没有声明,属于”死区“。如果 y 的默认值是 x,就不会报错,因为此时 x 已经声明了。
另外,下面的代码也会报错
1 | // 不报错 |
报错原因是在变量 x 还没有被 let 声明完成就使用,属于暂时性死区的问题。
块级作用域和函数声明
ES6 规定,块级作用域之中,函数声明语句的行为类似于 let ,在块级作用域之外不可引用。
下面看一段代码
1
2
3
4
5
6
7
8
9
10function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}()); 上面代码在 ES5 中运行,会得到“I am inside!”,因为在 if 内声明的函数 f 会被提升到函数头部,实际运行的代码如下。
1
2
3
4
5
6
7
8
9// ES5 环境
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}()); 在ES6中运行,结果如下:
1
2
3
4
5
6
7
8
9
10
11
12// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function 理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于 let ,对作用域之外没有影响。但是报错了,这是为什么呢?
原因是如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。
允许在块级作用域内声明函数。
函数声明类似于 var,即会提升到全局作用域或函数作用域的头部。
同时,函数声明还会提升到所在的块级作用域的头部。
根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于 var 声明的变量。上述代码实际运行如下:
1
2
3
4
5
6
7
8
9
10
11// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function 考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 函数声明语句
{
let a = 'secret';
function f() {
return a;
}
}
// 函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
2.const
const 和 let 一样不发生变量提升和存在暂时性死区的问题。
本质
const 实际上保证的,并不是变量的值不得改动,而是变量指向的内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的内存地址中,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针。const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
举个例子:
1
2
3
4
5
6
7
8
9
10const PI = 3.1415;
PI = 3;
// TypeError: Assignment to constant variable.
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only 上面代码中,常量 foo 储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把 foo 指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。